【为宏正名】什么?我忘了去上“数学必修课”!
【说在前面的话】
//! 非闰年的情况下,一年中有多少秒
#define SEC_IN_A_YEAR (60ul * 60ul * 24ul * 365ul)
static uint32_t s_wTotalSecInAYear = SEC_IN_A_YEAR;
例子虽然简单,但立马引出了一个有趣的问题:宏展开后,make时编译器看到的究竟是上述常量表达式的计算结果:
static uint32_t s_wTotalSecInAYear = 31536000ul;
还是原样的字符串替换呢?
static uint32_t s_wTotalSecInAYear = (60ul * 60ul * 24ul * 365ul);
感兴趣的读者可以通过“-E”来研究一下:
SET PATH=C:\Keil_v5\ARM\ARMCLANG\Bin;
armclang -xc -std=gnu11 --target=arm-arm-none-eabi -mcpu=cortex-m4 -E -o "preprocessed_main.c" "main.c"
这里,命令行使用 armclang(Arm Compiler 6)对 “main.c”进行预编译("-E"的结果),并将结果输出到一个名为“preprocessed_main.c” 的文件中——而这一文件就是我们在后面文章中要经常观察的,比如,针对前面的例子,一个可能的输出结果是:
# 1 "main.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 370 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "main.c" 2
# 24 "main.c"
static uint32_t s_wTotalSecInAYear = (60ul * 60ul * 24ul * 365ul);
【数位拼接律】
#define NUM_A 2
#define NUM_B 5
#define NUM_C 5
借助上一篇文章中引入的胶水宏 CONNECT3():
#define __CONNECT3(__A, __B, __C) __A##__B##__C
#define CONNECT3(__A, __B, __C) __CONNECT3(__A, __B, __C)
我们可以把这三个宏粘贴在一起:
#define NUM_COMBINE CONNECT3(NUM_A,NUM_B,NUM_C)
我们当然知道,最终宏替换的结果肯定是字符串“255”,但这个拼接出来的字符串“255”,和十进制数字255是等效的么?换句话说,预编译器懂得这个字符串“255”的含义么?为了验证这一问题,我们不妨使用下面的代码,去直接问问预编译器本人的看法:
#if NUM_COMBINE > 254
#warning larger than 254
#endif
#if NUM_COMBINE < 256
#warning smaller than 256
#endif
#if NUM_COMBINE == 0xFF
#warning equals to 0xFF
#endif
在 “main.c” 中加入上述全部宏定义以后,进行预编译,我们会得到如下的结果:
惊呆了!拼接出来的字符串不仅被正确的当作十进制数字256使用,还可以与十六进制数字进行正确的比较!!
这是不是意味着:无论是十进制、十六进制,我们只要想办法得到对应的“数位”,就可以通过拼接的方法还原出所需进制的“常熟字符串”,而且与编译器还懂得这一字符串的数学意义!——没错,这就是上一篇文章的最后,我们能够1)把任意通过宏编写的常量表达式计算出结果,并2)将数值转换成十进制字符串的原理——恍然大悟的同学可以“单击这里去重温一下”,这里就不再赘述了。
【序号自增】
预编译器能够理解“数字字符串”的数值意义;
宏的本质是一个对目标字符串的引用;
目标字符串是个常量,修改常量是不可能的;
推论:
假设一个宏表示一个序号
我们可以根据当前宏的值,计算出下一个序号的值,并借助“数位拼接律”生成一个新的字符串
修改宏的引用关系,让它指向新生成的字符串
根据上篇文章中引入的脚本头文件"mf_u8_dec2str.h",我们可以实现上述效果:
//! 一个用于表示序号的宏,初值是0
#define MY_INDEX 0
每次使用下面的预编译代码,我们就可以实现将 MY_INDEX的值加一的效果:
//! MFUNC_IN_U8_DEC_VALUE = MY_INDEX + 1; 给脚本提供输入
#define MFUNC_IN_U8_DEC_VALUE (MY_INDEX + 1)
//! 让预编译器执行脚本
#include "mf_u8_dec2str.h"
//! MY_INDEX = MFUNC_OUT_DEC_STR; 获得脚本输出
#define MY_INDEX MFUNC_OUT_DEC_STR
可以看到,虽然原理上可行,如果真用这种方法写代码,别说可读性差到爹妈都不认识,就算大家都能看懂,使用起来实在特别麻烦!否决!
typedef struct node_item_t node_item_t
struct node_item_t {
node_item_t *ptNext; //!< 指下一个元素
//! 链表节点的其它成员
uint8_t chID; //!< 假设有一个元素是序号
...
};
实际使用的时候,无论运行时刻链表的内容和结构是否会发生变化,但在编译时刻,我们会给他一些指定数量的初始的节点(比如16个),用数组来存储:
static node_item_t s_tItemPool[16];
static node_item_t *s_ptListRoot = NULL;
一般来说,我们需要编写一个初始化函数——在运行时刻将 s_tItemPool 中的元素一个一个手工加入到链表中(添加到 s_ptListRoot 指向的链表中)——这里的代价是双份的:
初始化函数所占用的代码空间
和
添加节点的运行时间。
借助__COUNTER__我们可以直接在编译时刻,以数组初始值的形式完成链表的初始化:
#define ADD_ITEM_TO(__LIST_ADDR, ...) \
{ \
.ptNext = &((__LIST_ADDR)[(__COUNTER__ + 1]), \
__VA_ARGS__ \
}
#define ADD_FINAL_ITEM(...) \
{ \
.ptNext = NULL, \
__VA_ARGS__ \
}
借助这个宏,我们可以实现对链表的静态初始化:
static node_item_t s_tItemPool[] = {
ADD_ITEM_TO(s_tItemPool), //!< 添加节点0
ADD_ITEM_TO(s_tItemPool), //!< 添加节点1
...
ADD_ITEM_TO(s_tItemPool), //!< 添加节点n-1
ADD_FINAL_ITEM(s_tItemPool), //!< 添加最后一个节点
};
static node_item_t *s_ptListRoot = s_tItemPool;
注意到节点内还有一个节点的序号“chID”,我们其实也可以一并将其自动初始化了——当然要记住,每次使用__COUNTER__它的值都会增加1——修改宏如下:
#define ADD_ITEM_TO(__LIST_ADDR, ...) \
{ \
.ptNext = &((__LIST_ADDR)[(__COUNTER__/2 + 1]), \
.chID = (__COUNTER__ / 2), \
__VA_ARGS__ \
}
#define ADD_FINAL_ITEM(__LIST_ADDR, ...) \
{ \
.ptNext = NULL, \
.chID = (__COUNTER__ / 2), \
__VA_ARGS__ \
}
修改后,实际展开效果如下:
static node_item_t s_tItemPool[] = {
{ .ptNext = &((s_tItemPool)[(0/2 + 1]), .chID = (1 / 2), },
{ .ptNext = &((s_tItemPool)[(2/2 + 1]), .chID = (3 / 2), },
...
{ .ptNext = &((s_tItemPool)[(4/2 + 1]), .chID = (5 / 2), },
{ .ptNext = NULL, .chID = (6 / 2), },
};
static node_item_t *s_ptListRoot = s_tItemPool;
上述效果虽然看似令人满意,但存在一个巨大的隐患,而这一隐患同样来自于__COUNTER__宏的基本特性:每次使用__COUNTER__它的值都会增加1——换句话说,在你使用 ADD_ITEM_TO() 的时候,如何才能确保 __COUNTER__是从0开始编号的呢?——别的宏可能已经使用过它了。
无论 __COUNTER__ 是什么值,我们都可以将其传递给一个枚举——作为初始值;
使用 __COUNTER__ 时,我们首先通过枚举将初始值扣除,从而获得“从0开始的计数”
#define __LIST_ROOT(__NAME) s_ptList##__NAME##Root
#define LIST_ROOT(__NAME) __LIST_ROOT(__NAME)
#define __IMP_LIST(__NAME) \
enum { \
/* 这里 "+1" 是把本次使用__COUNTER__也算进去 */ \
list_##__NAME##_start = __COUNTER__ + 1, \
}; \
static node_item_t s_tList##__NAME##Pool[] = {
#define __END_IMP_LIST(__NAME) \
}; \
static node_item_t *LIST_ROOT(__NAME) = \
s_tList##__NAME##Pool;
#define IMP_LIST(__NAME, ...) \
__IMP_LIST(__NAME, __VA_ARGS__)
#define END_IMP_LIST(__NAME) __END_IMP_LIST(__NAME)
#define __ADD_ITEM_TO(__NAME, ...) \
{ \
.ptNext = &(s_tList##__NAME##Pool[ \
(__COUNTER__ - list_##__NAME##_start)/2 + 1]), \
.chID = ((__COUNTER__ - list_##__NAME##_start) / 2), \
__VA_ARGS__ \
}
#define ADD_ITEM_TO(__NAME, ...) \
__ADD_ITEM_TO(__NAME, __VA_ARGS__)
#define __ADD_FINAL_ITEM(__NAME, ...) \
{ \
.ptNext = NULL, \
.chID = ((__COUNTER__ - list_##__NAME##_start) / 2), \
__VA_ARGS__ \
}
#define ADD_FINAL_ITEM(__NAME, ...) \
__ADD_FINAL_ITEM(__NAME, __VA_ARGS__)
为了方便隐藏定义枚举的“小动作”,我们追加了一对宏 IMP_LIST() 和 END_IMP_LIST(),就是"implement list"的缩写,它实现了以下功能:
以指定的名字定义了一个枚举;
以指定的名字定义了链表的节点池;
以指定的名字定义了指向链表的根指针,用户可以通过宏LIST_ROOT()来获取这一指针;
修改应用代码,实现一个叫做 MyList 的链表:
//! 实现一个list,名字叫 MyList
IMP_LIST(MyList)
ADD_ITEM_TO(MyList), //!< 添加节点0
ADD_ITEM_TO(MyList), //!< 添加节点1
...
ADD_ITEM_TO(MyList), //!< 添加节点n-1
ADD_FINAL_ITEM(MyList), //!< 添加最后一个节点
END_IMP_LIST(MyList)
是不是看起来很“优雅”?实际展开效果如下:
enum { list_MyList_start = 0 + 1, };
static node_item_t s_tListMyListPool[] = {
{ .ptNext = &(s_tListMyListPool[ (1 - list_MyList_start)/2 + 1]), .chID = ((2 - list_MyList_start) / 2), },
{ .ptNext = &(s_tListMyListPool[ (3 - list_MyList_start)/2 + 1]), .chID = ((4 - list_MyList_start) / 2), },
...
{ .ptNext = &(s_tListMyListPool[ (5 - list_MyList_start)/2 + 1]), .chID = ((6 - list_MyList_start) / 2), },
{ .ptNext = NULL, .chID = ((7 - list_MyList_start) / 2), },
};
static node_item_t *s_ptListMyListRoot = s_tListMyListPool;
【参数宏也支持重载?】
什么是参数宏的重载?——要回答这个问题,哪怕你连“重载(overload)”是什么都不知道也不要紧,我们来看一个最实际的例子:在前面的文章中,我们不止一次使用过一个胶水宏 CONNECT3,它的作用是将三个字符串粘连在一起变成一个完整的字符串。如果我们要粘连的字符串数量不同,比如,2个、4个、5个……n个,我们就要编写对应的版本:
#define __CONNECT2(__0, __1) __0##__1
#define __CONNECT3(__0, __1, __2) __0##__1##__2
#define __CONNECT4(__0, __1, __2, __3) __0##__1##__2##__3
...
#define __CONNECT8(__0, __1, __2, __3, __4, __5, __6, __7) \
__0##__1##__2##__3##__4##__5##__6##__7
#define __CONNECT9(__0, __1, __2, __3, __4, __5, __6, __7, __8) \
__0##__1##__2##__3##__4##__5##__6##__7##__8
//! 安全“套”
#define CONNECT2(__0, __1) __CONNECT2(__0, __1)
#define CONNECT3(__0, __1, __2) __CONNECT3(__0, __1, __2)
#define CONNECT4(__0, __1, __2, __3) __CONNECT4(__0, __1, __2, __3)
...
#define CONNECT8(__0, __1, __2, __3, __4, __5, __6, __7) \
__CONNECT8(__0, __1, __2, __3, __4, __5, __6, __7)
#define CONNECT9(__0, __1, __2, __3, __4, __5, __6, __7, __8) \
__CONNECT9(__0, __1, __2, __3, __4, __5, __6, __7, __8)
这里定义了最大连接9个的CONNECT版本,看似麻烦,实际上复制粘贴、一劳永逸——还是挺划算的——当然,如果你比较“耿直”,还可以做得更多,比如16个。所谓宏的重载是说:我们不必亲自去数要粘贴的字符串的数量而“手工选取正确的版本”,而直接让编译器自己替我们挑选。
比如,我们举一个组装16进制数字的例子:
#define HEX_U8_VALUE(__B1, __B0) \
CONNECT3(0x, __B1, __B0)
#define HEX_U16_VALUE(__B3, __B2, __B1, __B0) \
CONNECT5(0x, __B3, __B2, __B1, __B0)
#define HEX_U32_VALUE(__B7, __B6, __B4, __B4, __B3, __B2, __B1, __B0)\
CONNECT9(0x, __B7, __B6, __B4, __B4, __B3, __B2, __B1, __B0)
在支持重载的情况下,我们希望这样使用:
#define HEX_U8_VALUE(__B1, __B0) \
CONNECT(0x, __B1, __B0)
#define HEX_U16_VALUE(__B3, __B2, __B1, __B0) \
CONNECT(0x, __B3, __B2, __B1, __B0)
#define HEX_U32_VALUE(__B7, __B6, __B4, __B4, __B3, __B2, __B1, __B0)\
CONNECT(0x, __B7, __B6, __B4, __B4, __B3, __B2, __B1, __B0)
如你所见,无论实际给出的参数是多少个,我们都可以使用同一个参数宏CONNECT(),而CONNCT() 会自动计算用户给出参数的个数,从而正确的替换为CONNETn()版本。假设这一切都是可能做到的,那么实际上我们还可以对上述宏定义进行简化:
#define HEX_VALUE(...) CONNECT(0x, __VA_ARGS__)
#define HEX_U8_VALUE(__B1, __B0) \
HEX_VALUE(__B1, __B0)
#define HEX_U16_VALUE(__B3, __B2, __B1, __B0) \
HEX_VALUE(__B3, __B2, __B1, __B0)
#define HEX_U32_VALUE(__B7, __B6, __B4, __B4, __B3, __B2, __B1, __B0)\
HEX_VALUE(__B7, __B6, __B4, __B4, __B3, __B2, __B1, __B0)
是的,一个 HEX_VALUE() 就足够了,你随便添几个参数都行(只要小于等于你实现的CONNECTn的数量)。
#define VA_NUM_ARGS(...) /* 这里暂时先不管怎么实现 */
#define CONNECT(...) \
CONNECT2(CONNECT, VA_NUM_ARGS(__VA_ARGS__)) /*part1*/\
(__VA_ARGS__) /*part2*/
uint16_t hwValue = HEX_VALUE(D, E, A, D); //! 0xDEAD
uint16_t hwValue = CONNECT(0x, D, E, A, D);
uint16_t hwValue =
CONNECT2(CONNECT, VA_NUM_ARGS(0x, D, E, A, D))
(0x, D, E, A, D);
uint16_t hwValue =
CONNECT5
(0x, D, E, A, D);
#define VA_NUM_ARGS_IMPL(_1,_2,_3,_4,_5,_6,_7,_8,_9,__N,...) __N
#define VA_NUM_ARGS(...) \
VA_NUM_ARGS_IMPL(__VA_ARGS__,9,8,7,6,5,4,3,2,1)
在涉及"..."之前,它要用用户至少传递10个参数;
这个宏的返回值就是第十个参数的内容;
多出来的部分会被"..."吸收掉,不会产生任何后果
VA_NUM_ARGS() 的巧妙在于,它把__VA_ARGS__放在了参数列表的最前面,并随后传递了 "9,8,7,6,5,4,3,2,1" 这样的序号:
当__VA_ARGS__里有1个参数时,“1”对应第十个参数__N,所以返回值是1
当__VA_ARGS__里有2个参数时,“2”对应第十个参数__N,所以返回值是2
...
当__VA_ARGS__里有9个参数时,"9"对应第十个参数__N,所以返回值是9
如果觉得上述过程似懂非懂,我们不妨对前面的例子做一个展开:
VA_NUM_ARGS(0x, D, E, A, D)
VA_NUM_ARGS_IMPL(0x, D, E, A, D,9,8,7,6,5,4,3,2,1)
宏的重载非常有用,可以极大的简化用户"选择困难",你甚至可以将VA_NUM_ARGS() 与 函数名结合在一起,从而实现简单的函数重载(即,函数参数不同的时候,可以通过这种方法在编译阶段有预编译器根据用户输入参数的数量自动选择对应的函数),比如:
extern device_write1(const char *pchString);
extern device_write2(uint8_t *pchStream, uint_fast16_t hwLength);
extern device_write3(uint_fast32_t wAddress, uint8_t *pchStream, uint_fast16_t hwLength);
#define device_write(...) \
CONNECT2(device_write, VA_NUM_ARGS(__VA_ARGS__)) \
(__VA_ARGS__)
使用时:
device_write("hello world"); //!< 发送字符串
extern uint8_t chBuffer[32];
device_write(chBuffer, 32); //!< 发送缓冲
//! 向指定偏移量写数据
#define LCD_DISP_MEM_START 0x4000xxxx
extern uint16_t hwDisplayBuffer[320*240];
device_write(
LCD_DISP_MEM_START,
(uint8_t *)hwDisplayBuffer,
sizeof(hwDisplayBuffer)
);
原创不易,知识有价
如果你发现本文的知识对你有帮助、有启发,还请点赞、转发、收藏三连!
如果你喜欢我的思维,还 请订阅公众号【裸机思维】